/* Simple HTTP Server Example

   This example code is in the Public Domain (or CC0 licensed, at your option.)

   Unless required by applicable law or agreed to in writing, this
   software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
   CONDITIONS OF ANY KIND, either express or implied.
*/
#include <stdlib.h>
#include <stdint.h>

#include <esp_wifi.h>
#include <esp_event.h>
#include <esp_log.h>
#include <esp_system.h>
#include <nvs_flash.h>
#include <sys/param.h>
#include "freertos/timers.h"
#include "nvs_flash.h"
#include "esp_vfs_semihost.h"
#include "esp_vfs_fat.h"
#include "esp_spiffs.h"

#include "esp_netif.h"
#include "driver/gpio.h"
#include "cJSON.h"

#include <esp_http_server.h>

#include "web_wifi_config.h"
#include "web_ota.h"

// Set number of outputs
#define NUM_OUTPUTS  4

#define FILE_PATH_MAX (ESP_VFS_PATH_MAX + 128)
#define SCRATCH_BUFSIZE (10240)

#define REST_CHECK(a, str, goto_tag, ...)                                              \
    do                                                                                 \
    {                                                                                  \
        if (!(a))                                                                      \
        {                                                                              \
            ESP_LOGE(TAG, "%s(%d): " str, __FUNCTION__, __LINE__, ##__VA_ARGS__); \
            goto goto_tag;                                                             \
        }                                                                              \
    } while (0)

typedef struct rest_server_context {
    char base_path[ESP_VFS_PATH_MAX + 1];
    char scratch[SCRATCH_BUFSIZE];
} rest_server_context_t;

// Search for parameter in HTTP POST request
const char* PARAM_INPUT_1 = "ssid";
const char* PARAM_INPUT_2 = "pass";
const char* PARAM_INPUT_3 = "ip";
static bool s_sta_connected = false;

#define CHECK_FILE_EXTENSION(filename, ext) (strcasecmp(&filename[strlen(filename) - strlen(ext)], ext) == 0)

/* A simple example that demonstrates how to create GET and POST
 * handlers for the web server.
 */

static const char *TAG = "example_main";    
static httpd_handle_t server = NULL;

//Stupid li'l helper function that returns the value of a hex char.
static int httpdHexVal(char c)
{
    if (c >= '0' && c <= '9') {
        return c - '0';
    }

    if (c >= 'A' && c <= 'F') {
        return c - 'A' + 10;
    }

    if (c >= 'a' && c <= 'f') {
        return c - 'a' + 10;
    }

    return 0;
}

//Decode a percent-encoded value.
//Takes the valLen bytes stored in val, and converts it into at most retLen bytes that
//are stored in the ret buffer. Returns the actual amount of bytes used in ret. Also
//zero-terminates the ret buffer.
int httpdUrlDecode(char* val, int valLen, char* ret, int retLen)
{
    int s = 0, d = 0;
    int esced = 0, escVal = 0;

    while (s < valLen && d < retLen) {
        if (esced == 1)  {
            escVal = httpdHexVal(val[s]) << 4;
            esced = 2;
        } else if (esced == 2) {
            escVal += httpdHexVal(val[s]);
            ret[d++] = escVal;
            esced = 0;
        } else if (val[s] == '%') {
            esced = 1;
        } else if (val[s] == '+') {
            ret[d++] = ' ';
        } else {
            ret[d++] = val[s];
        }

        s++;
    }

    if (d < retLen) {
        ret[d] = 0;
    }

    return d;
}

//Find a specific arg in a string(/update?output=4&state=1) of get- or post-data.
//Line is the string of post/get-data, arg is the name of the value to find. The
//zero-terminated result is written in buff, with at most buffLen bytes used. The
//function returns the length of the result, or -1 if the value wasn't found. The
//returned string will be urldecoded already.
int httpd_find_arg(const char* line, const char* arg, char* buff, int buffLen)
{
    char* p, *e;
    // bool first_in = true;

    if (line == NULL) {
        return -1;
    }

    p = line;

    while (p != NULL && *p != '\n' && *p != '\r' && *p != 0) {
        // printf("findArg: %s\n", p);
        if (strncmp(p, arg, strlen(arg)) == 0 && p[strlen(arg)] == '=') {
            p += strlen(arg) + 1; //move p to start of value
            e = (char*)strstr(p, "&");

            if (e == NULL) {
                e = p + strlen(p);
            }

            // printf("findArg: val %s len %d\n", p, (e-p));
            return httpdUrlDecode(p, (e - p), buff, buffLen);
        }

        p = (char*)strstr(p, "&"); // Changed
        
        if (p != NULL) {
            p += 1;
        }
    }

    printf("Finding %s in %s: Not found\n", arg, line);
    return -1; //not found
}

/* Set HTTP response content type according to file extension */
static esp_err_t set_content_type_from_file(httpd_req_t* req, const char* filepath)
{
    const char* type = "text/plain";
    if (CHECK_FILE_EXTENSION(filepath, ".html")) {
        type = "text/html";
    } else if (CHECK_FILE_EXTENSION(filepath, ".js")) {
        type = "application/javascript";
    } else if (CHECK_FILE_EXTENSION(filepath, ".css")) {
        type = "text/css";
    } else if (CHECK_FILE_EXTENSION(filepath, ".png")) {
        type = "image/png";
    } else if (CHECK_FILE_EXTENSION(filepath, ".ico")) {
        type = "image/x-icon";
    } else if (CHECK_FILE_EXTENSION(filepath, ".svg")) {
        type = "text/xml";
    }
    return httpd_resp_set_type(req, type);
}

static esp_err_t custom_send_file_chunk(httpd_req_t* req, const char *filepath)
{
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    int fd = open(filepath, O_RDONLY, 0);
    if (fd == -1) {
        ESP_LOGE(TAG, "Failed to open file : %s", filepath);
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to read existing file");
        return ESP_FAIL;
    }

    set_content_type_from_file(req, filepath);

    char* chunk = rest_context->scratch;
    ssize_t read_bytes;
    do {
        /* Read file in chunks into the scratch buffer */
        read_bytes = read(fd, chunk, SCRATCH_BUFSIZE);
        if (read_bytes == -1) {
            ESP_LOGE(TAG, "Failed to read file : %s", filepath);
        } else if (read_bytes > 0) {
            /* Send the buffer contents as HTTP response chunk */
            if (httpd_resp_send_chunk(req, chunk, read_bytes) != ESP_OK) {
                close(fd);
                ESP_LOGE(TAG, "File sending failed!");
                /* Abort sending file */
                httpd_resp_sendstr_chunk(req, NULL);
                /* Respond with 500 Internal Server Error */
                httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to send file");
                return ESP_FAIL;
            }
        }
    } while (read_bytes > 0);
    /* Close file after sending complete */
    close(fd);
    ESP_LOGI(TAG, "File sending complete");
    /* Respond with an empty chunk to signal HTTP response completion */
    httpd_resp_send_chunk(req, NULL, 0);
    return ESP_OK;
}

/* Send HTTP response with the contents of the requested file */
static esp_err_t rest_common_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    if (req->uri[strlen(req->uri) - 1] == '/') {
        strlcat(filepath, "/index.html", sizeof(filepath));
    } else {
        strlcat(filepath, req->uri, sizeof(filepath));
    }

    char* p = strrchr(filepath, '?');
    if (p != NULL) {
        *p = '\0';
    }
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }
    
    return ESP_OK;
}

/* A help function to get post request data */
static esp_err_t recv_post_data(httpd_req_t* req, char* buf)
{
    int total_len = req->content_len;
    int cur_len = 0;
    int received = 0;

    if (total_len >= SCRATCH_BUFSIZE) {
        /* Respond with 500 Internal Server Error */
        httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "content too long");
        return ESP_FAIL;
    }
    while (cur_len < total_len) {
        received = httpd_req_recv(req, buf + cur_len, total_len);
        if (received <= 0) {
            /* Respond with 500 Internal Server Error */
            httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Failed to post control value");
            return ESP_FAIL;
        }
        cur_len += received;
    }
    buf[total_len] = '\0';//now ,the post is str format, like ssid=yuxin&pwd=TestPWD&chl=1&ecn=0&maxconn=1&ssidhidden=0
    ESP_LOGI(TAG, "Post data is : %s\n", buf);
    return ESP_OK;
}

static const char web_base_point[] = "/www";

esp_err_t init_fs(void)
{
    esp_vfs_spiffs_conf_t conf = {
        .base_path = web_base_point,
        .partition_label = NULL,
        .max_files = 5,//maybe the num can be set smaller
        .format_if_mount_failed = false
    };
    esp_err_t ret = esp_vfs_spiffs_register(&conf);

    if (ret != ESP_OK) {
        if (ret == ESP_FAIL) {
            ESP_LOGE(TAG, "Failed to mount or format filesystem");
        } else if (ret == ESP_ERR_NOT_FOUND) {
            ESP_LOGE(TAG, "Failed to find SPIFFS partition");
        } else {
            ESP_LOGE(TAG, "Failed to initialize SPIFFS (%s)", esp_err_to_name(ret));
        }
        return ESP_FAIL;
    }

    size_t total = 0, used = 0;
    ret = esp_spiffs_info(NULL, &total, &used);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "Failed to get SPIFFS partition information (%s)", esp_err_to_name(ret));
    } else {
        ESP_LOGI(TAG, "Partition size: total: %d, used: %d", total, used);
    }
    return ESP_OK;
}

static void timer_callback(TimerHandle_t timer)
{
    esp_restart();
}

static void create_a_restart_timer(void)
{
    TimerHandle_t oneshot = xTimerCreate("oneshot", 5000 / portTICK_PERIOD_MS, pdFALSE,
                                         NULL, timer_callback);
    xTimerStart(oneshot, 1);

    printf("Restarting in 5 seconds...\n");
    fflush(stdout);
}

/* Simple handler for uart_info_post handler */
static esp_err_t wifi_config_post_handler(httpd_req_t* req)
{
    ESP_LOGD(TAG, "in / post handler");
    wifi_config_t wifi_config;
    memset(&wifi_config, 0, sizeof(wifi_config_t));
    // wifi_config_store(&wifi_config);

    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    char* buf = ((rest_server_context_t*) (req->user_ctx))->scratch;
    int str_len = 0;
    char temp_str[64] = {0};
    if (recv_post_data(req, buf) != ESP_OK) {
        ESP_LOGE(TAG, "recv post data error");
        return ESP_FAIL;
    }

    str_len = httpd_find_arg(buf, PARAM_INPUT_1, temp_str, sizeof(temp_str));
    if ((str_len != -1) && (strlen((char *)temp_str) != 0)) {
        // snprintf((char *)wifi_config.sta.ssid, 32, "%s", temp_str);
        memcpy((char *)wifi_config.sta.ssid, temp_str, 32);
        ESP_LOGI(TAG, "ssid:%s", (char *)wifi_config.sta.ssid);
    }
    
    memset(temp_str, '\0', sizeof(temp_str));

    str_len = httpd_find_arg(buf, PARAM_INPUT_2, temp_str, sizeof(temp_str));
    if ((str_len != -1) && (strlen((char *)temp_str) != 0)) {
        memcpy((char *)wifi_config.sta.password, temp_str, 64);
        ESP_LOGI(TAG, "pwd:%s", (char *)wifi_config.sta.password);
    }
    if(!wifi_config_store(&wifi_config)) {
        return ESP_FAIL;
    }

    return ESP_OK;
}

static esp_err_t wifi_manage_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/wifimanager.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }

    return ESP_OK;
}

static esp_err_t softap_wifi_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/wifimanager_softap.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }

    return ESP_OK;
}

static esp_err_t ota_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/ota.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }

    return ESP_OK;
}

static esp_err_t reboot_html_get_handler(httpd_req_t* req)
{
    char filepath[FILE_PATH_MAX];
    rest_server_context_t* rest_context = (rest_server_context_t*) req->user_ctx;
    
    // return index html file
    strlcpy(filepath, rest_context->base_path, sizeof(filepath));
    strlcat(filepath, "/reboot.html", sizeof(filepath));
    if(custom_send_file_chunk(req, filepath) != ESP_OK) {
        ESP_LOGE(TAG, "rest common send err");
        return ESP_FAIL;
    }

    create_a_restart_timer();
    return ESP_OK;
}

esp_err_t start_webserver(const char* base_path)
{
    REST_CHECK(base_path, "wrong base path", err);
    rest_server_context_t* rest_context = calloc(1, sizeof(rest_server_context_t));
    REST_CHECK(rest_context, "No memory for rest context", err);
    strlcpy(rest_context->base_path, base_path, sizeof(rest_context->base_path));

    httpd_uri_t *uris = NULL;
    int uris_len = 0;
    httpd_handle_t server = NULL;
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_uri_handlers = 7;
    config.max_open_sockets = 7;
    config.uri_match_fn = httpd_uri_match_wildcard;
    config.stack_size = 5912;

    ESP_LOGI(TAG, "Starting HTTP Server");
    REST_CHECK(httpd_start(&server, &config) == ESP_OK, "Start server failed", err_start);

    httpd_uri_t httpd_uri_array_softap_only[] = {
        {"/wifi_config", HTTP_POST, wifi_config_post_handler, rest_context},
        {"/*", HTTP_GET, softap_wifi_html_get_handler,rest_context}
    };

    httpd_uri_t httpd_uri_array[] = {
        {"/wifimanager", HTTP_GET, wifi_manage_html_get_handler, rest_context},
        {"/ota", HTTP_GET, ota_html_get_handler, rest_context},
        {"/wifi_config", HTTP_POST, wifi_config_post_handler, rest_context},
        {"/update", HTTP_POST, OTA_update_post_handler, rest_context},
        {"/status", HTTP_POST, OTA_update_status_handler, rest_context},
        {"/reboot", HTTP_GET, reboot_html_get_handler, rest_context},
        {"/*", HTTP_GET, rest_common_get_handler,rest_context}//Catch-all callback function for the filesystem, this must be set to the array last one
    };

    if (!s_sta_connected) {
        uris = httpd_uri_array_softap_only;
        uris_len = sizeof(httpd_uri_array_softap_only)/sizeof(httpd_uri_t);
    } else {
        uris = httpd_uri_array;
        uris_len = sizeof(httpd_uri_array)/sizeof(httpd_uri_t);
    }

    for(int i = 0; i < uris_len; i++){
        if (httpd_register_uri_handler(server, &uris[i]) != ESP_OK) {
            ESP_LOGE(TAG, "httpd register uri_array[%d] fail", i);
        }
    }

    return ESP_OK;
err_start:
    free(rest_context);
err:
    return ESP_FAIL;
}

static void stop_webserver(httpd_handle_t server)
{
    // Stop the httpd server
    httpd_stop(server);
}

void app_main(void)
{
    // connect wifi
    app_wifi_main(&s_sta_connected);

    /* Start the server for the first time */
    ESP_ERROR_CHECK(init_fs());
    server = start_webserver(web_base_point);
}
